/**
	License:		Artistic license 2.0 (see license.txt, also available online at http://www.perlfoundation.org/artistic_license_2_0)
	Authors:		OrbitalLab (http://www.orbitallab.ru/moonglide/), 2007-2009

	File:			GuiBasic.d
	Description:	Texture cache system for gui: font glyph' bitmaps and icons/thumbnails
	Date:			17.05.2008 by Digited
*/
module moonglide.gui.GuiTextureCache;

import
	tango.util.container.LinkedList,

	moonglide.io.LogSystem,
	moonglide.math.MoonMath,
	moonglide.util.MoonUtil,

	moonglide.resources.Management;

const uint SIDE_SIZE = 256;

private
{
	Point2f[2] nullTexCoords;
}

//============================================================
/// quad of cache texture's surface
//============================================================
class CCacheTexQuad
{
	Point2i		intCoords,	// offset from (0,0)
				size;		// offset from intCoords
	Point2f[2]	uvCoords;

	bool		locked;

	this( Point2i int_coords, Point2i size_ )
	in
	{
		moonAssert( size.x > 0 && size.y > 0, "size is 0", __FILE__, __LINE__ );
	}
	body
	{
		intCoords = int_coords;
		size = size_;
	}

	static CCacheTexQuad opCall( Point2i int_coords, Point2i size_ )
	{
		return new CCacheTexQuad( int_coords, size_ );
	}
}


//============================================================
/// cached by cache manager. Can give real tex coords only when cache manager is locked
//============================================================
class CCachedImage
{
	public:
		CCacheTexQuad	cacheQuad;
		CResTextureData imageData;

		Point2f[] RealTexCooords() { return cacheQuad.locked ? nullTexCoords : cacheQuad.uvCoords; }

		this( CCacheTexQuad quad, CResTextureData data )
		in
		{
			moonAssert( quad !is null, "quad is null!", __FILE__, __LINE__ );
			moonAssert( data !is null, "data is null!", __FILE__, __LINE__ );
		}
		body
		{
			cacheQuad = quad;
			imageData = data;
		}
}

alias LinkedList!( CCacheTexQuad ) QuadsList;

//============================================================
/// cache page class
//============================================================
class CCachePage
{
	public:
		CResTexture	_mainTexture;
		bool		empty = true;
		uint		sideSize;

		Point2i		offset;		// general offset from main cache texture's (0,0)

		QuadsList	freeSurfaces;	// free space quads
		QuadsList	images;			// filled quads

		this( CResTexture cacheTex, uint side_size, Point2i maintex_offset )
		in
		{
			moonAssert( cacheTex is null, "cache texture given to cache page is null!", __FILE__, __LINE__ );
			moonAssert( side_size == 0, "size side is 0", __FILE__, __LINE__ );
		}
		body
		{
			images = new LinkedList!( CCacheTexQuad );
			freeSurfaces = new LinkedList!( CCacheTexQuad );

			_mainTexture = cacheTex;
			sideSize = side_size;
			offset = maintex_offset;
			freeSurfaces.append( CCacheTexQuad( maintex_offset, Point2i( side_size, side_size )));
		}
}


//============================================================
/// abstract icon packer class
//============================================================
abstract class CCachePacker
{
	public:
		CCachePage[]	pages;
		CCacheManager	manager;

		CCachedImage Pack( CResTexture image, bool lastPageOnly = false );
		void Clean();

		this( CCacheManager manager_ )
		{
			manager = manager_;
			pages ~= manager.NewPage;
		}
}


//============================================================
/// trivial box packer
//============================================================
class CBoxPacker : public CCachePacker
{
	public:
		override CCachedImage Pack( CResTexture image, bool lastPageOnly = false )
		in
		{
			moonAssert( image !is null, "Image is null!", __FILE__, __LINE__ );
			moonAssert( image.data.length == 0, "image data is null!", __FILE__, __LINE__ );
			moonAssert( pages !is null, "pages array is null", __FILE__, __LINE__ );
		}
		body
		{
			if( image.data[0].height > pages[0].sideSize || image.data[0].width > pages[0].sideSize )
			{
				errorLog( "BoxPacker can't cache image, bigger than cache page size" );
				return null;
			}

			CCachedImage cachedImage = null;

			CCacheTexQuad
				quad = new CCacheTexQuad( Point2i( 0, 0 ), Point2i( image.data[0].width, image.data[0].height )),
				bestMatch = null;

			uint bmIndex;

//------------------------------------------------------------------

				bool FindFreeQuad( CCachePage page )
				{
					foreach( freeQuad; page.freeSurfaces )
					{
						static int index = -1;
						index++;

						if( freeQuad.size.x >= quad.size.x && freeQuad.size.y >= quad.size.y )
						{
							static uint deltaMin;

							if( freeQuad.size.x == quad.size.x || freeQuad.size.y == quad.size.y )
							{
								// this is definitely best match, no reason to continue search
								bestMatch = freeQuad;
								break;
							}

							if( bestMatch )
							{
								static int newDelta;
								newDelta = min( freeQuad.size.x - quad.size.x, freeQuad.size.y - quad.size.y );

								// if we already have found good match, let's check which is better
								if( deltaMin > newDelta )
								{
									// this is a better match than previous!
									bestMatch = freeQuad;
									bmIndex = index;
									deltaMin = newDelta;
									continue;
								}
							}

							// we found the first candidate for best match
							bestMatch = freeQuad;
							bmIndex = index;
							deltaMin = min( freeQuad.size.x - quad.size.x, freeQuad.size.y - quad.size.y );
						}
					}

					return bestMatch !is null;
				}

//------------------------------------------------------------------
				// code moved to subfunction because of per-page search cycling
				void ProcessBestMatch( CCachePage page )
				{
					// 4. split best match
					uint
						bmX = bestMatch.size.x, bmY = bestMatch.size.y,
						qX = quad.size.x, qY = quad.size.y;

					bool
						bmSquare = bmX == bmY,
						splitVertical = true; // horizontal if false

					if( bmX == qX || bmY == qY )
						if( bmSquare )
						{
							// the simplest variant - BMQ equals image quad
							page.freeSurfaces.removeAt( bmIndex );
							page.images.append( bestMatch );
							delete quad;
							quad = bestMatch;
						}
						else
						{
							// simple variant - we need to split BMQ
							quad.intCoords = bestMatch.intCoords;

							if( bmX == qX )
							{
								// vertical split
								bestMatch.size.y -= qY;
								bestMatch.intCoords.y += qY;
							}
							else
							{
								// horizontal split
								bestMatch.size.x -= qX;
								bestMatch.intCoords.x += qX;
							}

							page.images.append( quad );
						}
					else
					{
						if( !bmSquare )
							// find the best of two variants of splitting BMQ
							// by biggest sum of aspect ratios of resulting free quads
							splitVertical = cast(float)max( qX, bmY - qY ) / cast(float)min( qX, bmY - qY ) +
								cast(float)max( bmX - qX, bmY ) / cast(float)min( bmX - qX, bmY ) >
								cast(float)max( bmX - qX, bmY - qY ) / cast(float)min( bmX - qX, bmY - qY ) +
								cast(float)max( bmX, bmY - qY ) / cast(float)min( bmX, bmY - qY );

						quad.intCoords = bestMatch.intCoords;
						CCacheTexQuad newFreeQuad;

						// split
						if( splitVertical )
						{
							newFreeQuad = new CCacheTexQuad(
								Point2i( bestMatch.intCoords.x, bestMatch.intCoords.y + qY ),
								Point2i( qX, bmY - qY )); // first resulting free quad
							bestMatch.intCoords.x += qX;  // second (transformed best match quad)
							bestMatch.size.x -= qX;
						}
						else
						{
							newFreeQuad = new CCacheTexQuad(
								Point2i( bestMatch.intCoords.x, bestMatch.intCoords.y + qY ),
								Point2i( bmX, bmY - qY ));
							bestMatch.intCoords.x += qX;
							bestMatch.size.x -= qX;
							bestMatch.size.y = qY;
						}

						page.freeSurfaces.append( newFreeQuad );
						page.images.append( quad );
					}

//---------- SUCCESS -----------------------------------------------
					// set quad uvCoords and create a cached image object from it
					quad.uvCoords[0] = manager.CoordsToUV( quad.intCoords.x, quad.intCoords.y );
					quad.uvCoords[1] = manager.CoordsToUV( quad.intCoords.x + quad.size.x, quad.intCoords.y + quad.size.y );

					cachedImage = new CCachedImage( quad, image.data[0] );
					manager.ModifyCacheTexRegion( cachedImage );
				}

//------------------------------------------------------------------

			if( lastPageOnly )
				if( FindFreeQuad( pages[$-1] ))
					ProcessBestMatch( pages[$-1] );
			else
				foreach( page; pages )
					if( FindFreeQuad( page ))
					{
						ProcessBestMatch( page );
						break;
					}

			if( cachedImage is null )
			{
				pages ~= manager.NewPage;
				return this.Pack( image, true );
			}

			return cachedImage;
		}


		override void Clean()
		{
			// clean all pages and release all except first
		}

		this( CCacheManager manager_ )
		{
			super( manager_ );
		}
}


//============================================================
/// main texture cache class - manages texture cache
//============================================================
class CCacheManager
{
	protected:
		CCachePacker[]		_packers;
		CCachePage[]		_pages;

		uint				_pageSize,
							_multiplifier = 1;	// times mainTexture size bigger than pageSize

		CResTexture			_mainTexture;

	public:
		invariant()
		{
			// _multiplifier must be 2^n (n == 0..inf)
			moonAssert((( _multiplifier == 1 ) || ( _multiplifier == 2 )) || ( 0 == ( _multiplifier >>> 1 ) % 2 ), "multiplifier error", __FILE__, __LINE__ );
		}

		this( uint side_size = SIDE_SIZE )
		in
		{
			moonAssert( side_size != 0, "page size = 0!", __FILE__, __LINE__ );
		}
		body
		{
			_pageSize = side_size;

			// create cache texture
			_mainTexture = CResTexture( "MainGuiCacheTexture" );
			_mainTexture.data ~= CResTextureData( "MainGuiCacheTexture_data" );

			debug moonAssert( _mainTexture.data[0] !is null, "cast from ResManager.new returned null", __FILE__, __LINE__ );

			_mainTexture.data[0].Resize( _pageSize, _pageSize );
		}

		~this()
		{
			// destroy cache texture
		}

		void ModifyCacheTexRegion( CCachedImage image )
		{
			debug moonAssert( _mainTexture !is null, "", __FILE__, __LINE__ );
			debug moonAssert( _mainTexture.data.length > 0, "", __FILE__, __LINE__ );

			// fill cache texture with data from image
			_mainTexture.data[0].CopyToTexRegion( image.imageData, image.imageData.GetAllRegion,
				Point2i( image.cacheQuad.intCoords.x, image.cacheQuad.intCoords.y ));
		}

//----------------------------------------------------------

		// when locked, can't add/delete/move images, cached at main cache texture;
		void Lock()
		{
			foreach( inout page; _pages )
				foreach( inout image; page.images )
					image.locked = true ;
		}

		// when unlocked, can't permit cached textures to give real tex coords because they can be changed later
		void Unlock()
		{
			foreach( inout page; _pages )
				foreach( inout image; page.images )
					image.locked = false;
		}

//----------------------------------------------------------

		//============================================================
		/// get new quad page from cache texture
		//============================================================
		CCachePage NewPage()
		{
			CCachePage newpage = new CCachePage( _mainTexture, SIDE_SIZE, Point2i(0,0) );

			// 1. check if we need to enlarge main texture
			uint len = _pages.length;
			bool haveToEnlargeCacheTex = false;

			if( len > 3 )
				if(( len == 4 ) || ( 0 == ( len >>> 2 ) % 4 )) // must be 4 * 2^n
					haveToEnlargeCacheTex = true; // have to enlarge mainTexture and set newpage properties from enlarged part

			if( haveToEnlargeCacheTex )
			{
				//
			}
			else
			{
				//
			}

			_pages ~= newpage;

			return newpage;
		}

		//============================================================
		/// uint coords to float [0..1] UV coords
		//============================================================
		Point2f CoordsToUV( uint x, uint y )
		in
		{
			moonAssert( _mainTexture  !is null, "main cache texture is null!", __FILE__, __LINE__ );
		}
		body
		{
			return Point2f(
				cast(float)x / cast(float)_mainTexture.data[0].width,
				cast(float)y / cast(float)_mainTexture.data[0].height );
		}
}
